import {
    DayFrequency,
    DayRangeRule,
    Frequency,
    Head,
    HourFrequency,
    HourRangeRule,
    MinuteFrequency,
    RuleChain,
    SecondFrequency,
    Tail,
    TimePointRule,
} from "./rule";

import {
    day2num,
    DayOfWeek,
    DayOfWeekNum,
    dayRange,
    TimePoint,
    timePointReg,
    TimePointTemplate,
} from "./time-type";

interface Start {
    at(
        timePoint: TimePointTemplate,
        ...timePoints: TimePointTemplate[]
    ): TimePointAnd & DayRangeIn;
    every(x: number): XUnit;
    every_second(): HourRangeIn & DayRangeIn;
    every_minute(): HourRangeIn & DayRangeIn;
    every_hour(): HourRangeIn & DayRangeIn;
    every_day(): DayRangeIn;
}

interface TimePointAnd {
    and(timePoint: string): DayRangeIn;
}

interface DayRangeIn {
    on(day: DayOfWeek): DayRangeAnd & DayRangeTo;
    on(day: DayOfWeek, day2: DayOfWeek, ...days: DayOfWeek[]): DayRangeAnd;
    from(day: DayOfWeek): DayRangeTo;
    except(day: DayOfWeek): DayRangeAnd & DayRangeTo;
    except(day: DayOfWeek, day2: DayOfWeek, ...days: DayOfWeek[]): DayRangeAnd;
}

interface DayRangeTo {
    to(day: DayOfWeek): DayRangeAnd;
}

interface DayRangeAnd {
    and(day: DayOfWeek): DayRangeTo;
}

interface XUnit {
    seconds(): HourRangeIn & DayRangeIn;
    minutes(): HourRangeIn & DayRangeIn;
    hours(): HourRangeIn & DayRangeIn;
    days(): DayRangeIn;
}

interface HourRangeIn {
    from(timePont: TimePointTemplate): HourRangeTo;
    except(timePont: TimePointTemplate): HourRangeTo;
}

interface HourRangeTo {
    to(timePoint: TimePointTemplate): HourRangeAnd & DayRangeIn;
}

interface HourRangeAnd {
    and(timePoint: TimePointTemplate): HourRangeTo;
}

interface TimedRuleWordsWrapper {
    get timeRuleWords(): string[];
}

function TimedRuleString2Words(rule: string): string[] {
    return rule.trim().replace(/,|，/g, " , ").toLowerCase().split(/\s+/);
}

interface Timed {
    timed(rule: string): void;
}

class RuleBuilder implements Start, Timed {
    private _timeRuleWords: string[];
    constructor() {
        this._timeRuleWords = [];
    }

    get timeRuleWords() {
        return this._timeRuleWords;
    }

    timed(rule: string) {
        this._timeRuleWords = TimedRuleString2Words(rule);
    }

    at(timePoint: string, ...timePoints: string[]) {
        this._timeRuleWords.push("at", timePoint, ...timePoints);
        return this;
    }

    every(x: number) {
        this._timeRuleWords.push("every", String(x));
        return this;
    }

    seconds() {
        this._timeRuleWords.push("seconds");
        return this;
    }

    minutes() {
        this._timeRuleWords.push("minutes");
        return this;
    }

    hours() {
        this._timeRuleWords.push("hours");
        return this;
    }

    days() {
        this._timeRuleWords.push("days");
        return this;
    }

    every_second() {
        this._timeRuleWords.push("every", "second");
        return this;
    }

    every_minute() {
        this._timeRuleWords.push("every", "minute");
        return this;
    }

    every_hour() {
        this._timeRuleWords.push("every", "hour");
        return this;
    }

    every_day() {
        this._timeRuleWords.push("every", "day");
        return this;
    }

    and(dayOrPoint: string | DayOfWeek) {
        this._timeRuleWords.push("and", dayOrPoint);
        return this;
    }

    on(day: DayOfWeek, day2?: DayOfWeek, ...days: DayOfWeek[]) {
        this._timeRuleWords.push("on", day);
        if (day2 != undefined) {
            this._timeRuleWords.push(day2, ...days);
        }
        return this;
    }

    from(timePoint: string) {
        this._timeRuleWords.push("from", timePoint);
        return this;
    }

    except(
        dayOrPoint: DayOfWeek | TimePointTemplate,
        day2?: DayOfWeek,
        ...days: DayOfWeek[]
    ) {
        this._timeRuleWords.push("except", dayOrPoint);
        if (day2 != undefined) {
            this._timeRuleWords.push(day2, ...days);
        }
        return this;
    }

    to(dayOrPoint: DayOfWeek | string) {
        this._timeRuleWords.push("to", dayOrPoint);
        return this;
    }
}

function createRuleBuilder(): Start & Timed {
    return new RuleBuilder() as Start & Timed;
}

/**
 * 解析自然语言描述的时间规则，并返回 RuleChain 对象描述的时间规则链
 * @param ruleWords 自然语言字符串或单词列表描述的时间规则
 * @returns RuleChain 对象表示的时间规则链，RuleChain 当前可能包括一个规则，以后可能会包括多个
 */
function parseFrom(ruleWords: string[]): RuleChain {
    let headRule: Head;
    let i = 0;
    // 解析出时间点或天级、小时级、分钟级频率
    switch (ruleWords[i]) {
        case "at":
            [i, headRule] = parseTimePointsRuleFromWords(ruleWords, i + 1);
            break;
        case "every":
            [i, headRule] = parseFrequencyRuleFromWords(ruleWords, i + 1);
            break;
        default:
            throw new Error("解析错误，无法解析出合法的固定时间点或频率");
    }

    let chain = new RuleChain(headRule);

    if (i >= ruleWords.length) {
        return chain;
    }

    // 继续解析所在/排除时间范围
    let tailRule: Tail;
    let isExclude: boolean = false;
    if (ruleWords[i] === "except") {
        isExclude = true;
        i += 1;
    }

    if (ruleWords[i] === "from" || ruleWords[i] === "on") {
        i += 1;
    }

    if (ruleWords[i] in day2num) {
        tailRule = parseDayRangeRuleFromWords(ruleWords, i, isExclude);
    } else if (timePointReg.test(ruleWords[i])) {
        tailRule = parseHourRangeRuleFromWords(ruleWords, i, isExclude);
    } else {
        throw new Error("尝试解析时间范围出错");
    }

    chain.setTailRule(tailRule);
    return chain;
}

/**
 * 从单词列表中解析出固定时间点规则，解析过程从下标 i 开始，直到列表末尾或遇到不能解析的词时结束
 * @param words 单词列表
 * @param i 解析过程的起始下标
 * @returns 解析结束下标和固定时间点规则对象组成的二元列表
 */
function parseTimePointsRuleFromWords(
    words: string[],
    i: number
): [number, TimePointRule] {
    let timePoints: TimePoint[] = [];
    while (i < words.length) {
        if (words[i] === "and" || words[i] === ",") {
            i += 1;
            continue;
        } else if (timePointReg.test(words[i])) {
            let [h, m, s] = words[i].split(":");
            if (s === undefined) {
                s = "0";
            }

            timePoints.push(new TimePoint(+h, +m, +s));
        } else {
            break;
        }

        i++;
    }

    return [i, new TimePointRule(timePoints)];
}

/**
 * 从单词列表解析出频率规则，解析过程从下标 i 开始
 * @param words 单词列表
 * @param i 解析过程的起始下标
 * @returns 解析结束时的下标和频率规则对象组成二元列表
 */
function parseFrequencyRuleFromWords(
    words: string[],
    i: number
): [number, Frequency] {
    let rule: Frequency;

    if (/^\d+$/.test(words[i])) {
        let count = +words[i];
        i += 1;
        switch (words[i]) {
            case "days":
                rule = new DayFrequency(count);
                break;
            case "hours":
                rule = new HourFrequency(count);
                break;
            case "minutes":
                rule = new MinuteFrequency(count);
                break;
            case "seconds":
                rule = new SecondFrequency(count);
                break;
            default:
                throw new Error("解析频率规则错误");
        }
    } else {
        switch (words[i]) {
            case "day":
                rule = new DayFrequency(1);
                break;
            case "hour":
                rule = new HourFrequency(1);
                break;
            case "minute":
                rule = new MinuteFrequency(1);
                break;
            case "second":
                rule = new SecondFrequency(1);
            default:
                throw new Error("解析频率规则出错");
        }
    }

    return [i + 1, rule];
}

/**
 * 从单词列表解析出天级时间范围规则，解析过程从下标 i 开始
 * @param words 单词列表
 * @param i 解析过程的起始下标
 * @param isExclude 如果为 true，表示排除时间范围；否则，为所在时间范围
 * @returns 解析结束时的下标和天级时间范围规则对象组成二元列表
 */
function parseDayRangeRuleFromWords(
    words: string[],
    i: number,
    isExclude: boolean = false
): DayRangeRule {
    let days: DayOfWeekNum[] = [];
    while (i < words.length) {
        if (words[i] in day2num) {
            if (i + 1 < words.length && words[i + 1] === "to") {
                days.splice(
                    days.length,
                    0,
                    ...dayRange(
                        words[i] as DayOfWeek,
                        words[i + 2] as DayOfWeek
                    )
                );
                i += 3;
            } else {
                days.push(day2num[words[i] as DayOfWeek]);
                i += 1;
            }
        } else if (words[i] === "and" || words[i] === ",") {
            i += 1;
            continue;
        }
    }

    let dayRangeRule: DayRangeRule;
    if (isExclude) {
        dayRangeRule = new DayRangeRule(true);
        dayRangeRule.excludeDays(...days);
    } else {
        dayRangeRule = new DayRangeRule(false);
        dayRangeRule.addDays(...days);
    }

    return dayRangeRule;
}

/**
 * 从单词列表解析出小级时间范围规则，解析过程从下标 i 开始
 * @param words 单词列表
 * @param i 解析过程的起始下标
 * @param isExclude 如果为 true，表示排除时间范围；否则，为所在时间范围
 * @returns 解析结束时的下标和小时级时间范围规则对象组成二元列表
 */
function parseHourRangeRuleFromWords(
    words: string[],
    i: number,
    isExclude: boolean = false
): HourRangeRule {
    let rule: HourRangeRule = new HourRangeRule(!isExclude);
    while (i < words.length) {
        let hourRanges;
        [i, hourRanges] = parseTimeRangeFromWords(words, i);
        let days;
        [i, days] = parseDayRangeFromWords(words, i);
        for (let hourRange of hourRanges) {
            rule.addRange(hourRange, ...days);
        }
    }
    return rule;
}

/**
 * 从单词列表解析出间范围列表
 * @param words 单词列表
 * @param i 解析过程的起始下标
 * @returns 解析结束时的下标和时间范围列表组成二元列表，时间范围是两个 TimePoint 组成二元列表，
 * 第一个 TimePoint 表示时间范围起点，第二个 TimePoint 表示时间范围结束点
 */
function parseTimeRangeFromWords(
    words: string[],
    i: number
): [number, [TimePoint, TimePoint][]] {
    let ranges: [TimePoint, TimePoint][] = [];

    while (i < words.length) {
        // 解析出时间范围
        if (timePointReg.test(words[i])) {
            // 解析start时间点
            let [h, m, s] = words[i].split(":");
            if (s === undefined) {
                s = "0";
            }
            let startPoint: TimePoint = new TimePoint(+h, +m, +s);
            i += 1;
            if (words[i] != "to") {
                throw new Error(
                    `解析时间点是期待出现一个\'to\',但出现的是${words[i]}`
                );
            }
            i += 1;
            if (i >= words.length) {
                throw new Error("期待解析出to后的时间点，但是单词已结束");
            }

            if (timePointReg.test(words[i])) {
                // 解析end 时间点
                let [h, m, s] = words[i].split(":");
                if (s === undefined) {
                    s = "0";
                }
                let endPoint: TimePoint = new TimePoint(+h, +m, +s);
                ranges.push([startPoint, endPoint]);
                i += 1;
            } else {
                throw new Error(
                    `期待解析出to后的时间点，但是遇到的是${words[i]}`
                );
            }
        } else if (
            words[i] == "and" ||
            words[i] == "," ||
            words[i] == "，" ||
            words[i] == "from"
        ) {
            i += 1;
            continue;
        } else if (words[i] == "on" || words[i] in day2num) {
            break;
        } else {
            throw Error(`解析时间点范围时遇到无法理解的词${words[i]}`);
        }
    }

    return [i, ranges];
}

/**
 * 从单词列表解析出天极时间列表
 * @param words 单词列表
 * @param i 解析过程的起始下标
 * @returns 解析结束时的下标和天极时间列表组成二元列表
 */
function parseDayRangeFromWords(
    words: string[],
    i: number
): [number, DayOfWeekNum[]] {
    let days: DayOfWeekNum[] = [];
    while (i < words.length) {
        if (
            words[i] === "and" ||
            words[i] === "on" ||
            words[i] === "from" ||
            words[i] === ","
        ) {
            i += 1;
            continue;
        }

        if (words[i] in day2num) {
            let day = day2num[words[i] as DayOfWeek];
            if (i + 1 < words.length && words[i + 1] === "to") {
                days.splice(
                    days.length,
                    0,
                    ...dayRange(
                        words[i] as DayOfWeek,
                        words[i + 2] as DayOfWeek
                    )
                );
                i += 3;
            } else {
                days.push(day);
                i += 1;
            }
        } else if (timePointReg.test(words[i])) {
            break;
        } else {
            throw new Error(`解析天级时间范围时遇到不能理解的词:${words[i]}`);
        }
    }
    return [i, days];
}

export {
    createRuleBuilder,
    TimedRuleWordsWrapper,
    parseFrom,
    RuleBuilder,
    Start,
    Timed,
};
